﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Controls;
using Silvermoon.UI;

namespace Silvermoon.Navigation
{
    /// <summary>
    /// Maintains navigation between pages.
    /// </summary>
    public class NavigationService
    {
        #region fields
        private PageNode CurrentPageNode;

        private PageNode ParentPageNode { get { return CurrentPageNode != null ? CurrentPageNode.Parent : null; } }

        private PageTransitionMode navigationMode = PageTransitionMode.ShowAndHide;
        private NavigationType currentNavigationKind = NavigationType.None;
        private Page pageFrom;
        private Page pageToHide, pageToShow;
        private VisualState showState;
        #endregion
        #region props

        /// <summary>
        /// Gets whether the current page is the root node.
        /// </summary>
        public bool IsRoot
        {
            get { return CurrentPageNode.Parent == null; }
        }

        public bool IsInTransition
        {
            get { return pageToShow != null || pageToHide != null; }
        }

        /// <summary>
        /// Gets the current Page.
        /// </summary>
        public Page CurrentPage { get { return CurrentPageNode != null ? CurrentPageNode.Page : null; } }

        /// <summary>
        /// Gets whether a navigation between pages is still in progress.
        /// </summary>
        public bool IsNavigating
        {
            get { return currentNavigationKind != NavigationType.None; }
        }

        #endregion
        #region methods


        /// <summary>
        /// Navigates forward to another page.
        /// </summary>
        /// <param name="target">Page to navigate to.</param>
        /// <returns>True if navigation started, otherwise false.</returns>
        public bool NavigateTo(Page target)
        {
            if (target == null) throw new ArgumentNullException("target");
            if (target == CurrentPage || IsInTransition) return false;


            target.NavigationService = this;

            Page currentPage = CurrentPage;
            Type fromType = currentPage != null ? currentPage.GetType() : typeof(Page);
            PageTransition transition = GetTransition(fromType, target.GetType());
            if (transition == null) throw new ArgumentOutOfRangeException();

            this.navigationMode = transition.Mode;


            PageNode newNode = new PageNode(target);
            newNode.Parent = CurrentPageNode;

            if (currentPage != null && CurrentPageNode != null) CurrentPageNode.ViewState = currentPage.OnSaveViewState();
            pageFrom = currentPage;
            CurrentPageNode = newNode;

            if (ParentPageNode != null)
            {
                currentPage = ParentPageNode.Page;
                this.currentNavigationKind = NavigationType.NavigateTo;
                StartNavigation(transition.Mode, currentPage, target, transition.Source, transition.Target);
            }
            else
            {
                target.Show(transition.Target);
            }
            return true;
        }

        /// <summary>
        /// Navigates back to the previous page.
        /// </summary>
        /// <returns>True if navigation started, otherwise false.</returns>
        public bool GoBack()
        {
            if (IsInTransition || ParentPageNode == null) return false;
            Page currentPage = CurrentPageNode.Page;

            PageNode parentNode = ParentPageNode;
            NavigateBack(currentPage, parentNode);
            return true;
        }

        /// <summary>
        /// Navigates several pages back.
        /// </summary>
        /// <param name="level">Number of pages to go back.</param>
        /// <returns>True if navigation started, otherwise false.</returns>
        public bool GoBack(int level)
        {
            if (level <= 1) throw new ArgumentOutOfRangeException("level");
            if (IsInTransition || ParentPageNode == null) return false;
            while (level > 1)
            {
                level--;
                RemoveParentFromStack();
            }
            return GoBack();
        }

        /// <summary>
        /// Navigates to the home page.
        /// </summary>
        /// <returns>True if navigation started, otherwise false.</returns>
        public bool GoHome()
        {
            if (IsInTransition) return false;

            Page currentPage = CurrentPage;
            PageNode home = CurrentPageNode.Parent;
            if (home != null)
            {
                while (home.Parent != null)
                {
                    Page page = home.Page;
                    if (page != null && (!page.KeepAlive)) page.Dispose();
                    home = home.Parent;
                }
                NavigateBack(currentPage, home);
                return true;
            }
            return false;
        }

        private void NavigateBack(Page currentPage, PageNode parentNode)
        {
            pageFrom = currentPage;
            Page parentPage = parentNode.Page;
            if (parentPage == null)
            {
                parentPage = parentNode.ActivatePage(this);
                if (parentPage == null) throw new ArgumentNullException("Activator", "Could not create page from activator.");
                parentNode.Page = parentPage;
                object state = parentNode.ViewState;
                if (state != null) parentPage.OnLoadViewState(state);
            }
            CurrentPageNode = parentNode;
            PageTransition transition = GetTransition(currentPage.GetType(), parentPage.GetType());
            if (transition == null) throw new ArgumentOutOfRangeException("transition");
            this.navigationMode = transition.Mode;
            StartNavigation(transition.Mode, currentPage, parentPage, transition.Source, transition.Target);
        }

        private void StartNavigation(PageTransitionMode type, Page currentPage, Page target, VisualState hideState, VisualState showState)
        {
            target.PreLoad();
            this.showState = showState;
            target.NavigationService = this;
            target.OnNavigateFrom(currentPage);
            currentPage.OnNavigateTo(target);
            switch (type)
            {
                case PageTransitionMode.HideThenShow:
                    pageToHide = currentPage;
                    pageToShow = target;
                    if (hideState != null) currentPage.Hide(hideState); else currentPage.Hide();
                    break;

                case PageTransitionMode.HideAndShow:
                    pageToHide = currentPage;
                    pageToShow = target;
                    if (showState != null) target.Show(showState); else target.Show();
                    target.SendToBack();
                    if (hideState != null) currentPage.Hide(hideState); else currentPage.Hide();
                    break;


                case PageTransitionMode.ShowAndHide:
                    pageToHide = currentPage;
                    pageToShow = target;
                    if (showState != null) target.Show(showState); else target.Show();
                    //target.BringToFront();
                    if (hideState != null) currentPage.Hide(hideState); else currentPage.Hide();
                    break;

                case PageTransitionMode.Show:
                    pageToShow = target;
                    if (showState != null) target.Show(showState); else target.Show();
                    break;

                case PageTransitionMode.Hide:
                    pageToHide = currentPage;
                    target.Open(true);
                    target.SendToBack();
                    if (hideState != null) currentPage.Hide(hideState); else currentPage.Hide();
                    break;

                default:
                    throw new NotImplementedException();
            }
        }


        internal void OnPageShown(Page page)
        {
            if (page == pageToShow)
            {
                pageToShow = null;
                switch (navigationMode)
                {
                    case PageTransitionMode.Show:
                        CloseAndDisposePageNode();
                        break;
                }
                Complete();
            }
        }

        internal void OnPageHidden(Page page)
        {
            if (page == pageToHide)
            {
                pageToHide = null;
                switch (navigationMode)
                {
                    case PageTransitionMode.Hide:
                    case PageTransitionMode.HideAndShow:
                    case PageTransitionMode.ShowAndHide:
                        CloseAndDisposePageNode();
                        break;

                    case PageTransitionMode.HideThenShow:
                        CloseAndDisposePageNode();
                        VisualState state = showState;
                        if (state != null) CurrentPage.Show(state); else CurrentPage.Show();
                        break;

                }
                Complete();
            }
        }

        private void CloseAndDisposePageNode()
        {
            if (pageFrom != null)
            {
                pageFrom.Close();
                if (!pageFrom.KeepAlive)
                {
                    PageNode parent = ParentPageNode;
                    if (parent != null && this.currentNavigationKind != NavigationType.Back)
                    {
                        parent.Page = null;
                    }
                    pageFrom.Dispose();
                }
                pageFrom = null;
            }
        }

        private void Complete()
        {
            if (!IsInTransition)
            {
                currentNavigationKind = NavigationType.None;
            }
        }

        public void RemoveFromStack(Page page)
        {
            if (page == null) throw new ArgumentNullException("page");

            PageNode node = CurrentPageNode;
            while (node != null && node.Parent != null && node.Parent.Page != page) node = node.Parent;
            if (node != null && node.Parent != null && node.Parent.Page == page)
            {
                node.Parent = node.Parent.Parent;
            }
        }

        private void RemoveParentFromStack()
        {
            var parent = CurrentPageNode.Parent;
            CurrentPageNode.Parent = parent.Parent;
            if (parent.Page != null) parent.Page.Dispose();
        }

        #endregion
        #region statics

        private static Dictionary<PageTypeKey, PageTransition> pageTransitions = new Dictionary<PageTypeKey, PageTransition>();

        static NavigationService()
        {
            Type page = typeof(Page);

            PageTypeKey key = new PageTypeKey(page, page);
            pageTransitions.Add(key, PageTransitions.Default);
        }

        /// <summary>
        /// Sets the transition from one page type to another.
        /// </summary>
        /// <param name="from">Ttype of the  page to navigate from.</param>
        /// <param name="to">Type of the page to navigate to.</param>
        /// <param name="transition">Transition to use. If null, an existing transition is removed.</param>
        public static void SetTransition(Type from, Type to, PageTransition transition)
        {
            if (from == null) throw new ArgumentNullException("from");
            if (to == null) throw new ArgumentNullException("to");

            Type t = typeof(Page);
            if (!t.IsAssignableFrom(from)) throw new ArgumentOutOfRangeException("from", "type is not assignable from Page.");
            if (!t.IsAssignableFrom(to)) throw new ArgumentOutOfRangeException("to", "type is not assignable from Page.");

            SetTransitionInternal(from, to, transition);
        }

        /// <summary>
        /// Sets the transition from one page type to another.
        /// </summary>
        /// <typeparam name="FROM">Type of the page to navigate from.</typeparam>
        /// <typeparam name="TO">Type of the page to navigate to.</typeparam>
        /// <param name="transition">Transition to use. If null, an existing transition is removed.</param>
        public static void SetTransition<FROM, TO>(PageTransition transition)
            where FROM : Page
            where TO : Page
        {
            Type from = typeof(FROM);
            Type to = typeof(TO);
            SetTransition(from, to, transition);
        }


        private static void SetTransitionInternal(Type from, Type to, PageTransition transition)
        {
            PageTypeKey key = new PageTypeKey(from, to);
            if (transition == null)
            {
                pageTransitions.Remove(key);
            }
            else
            {
                pageTransitions[key] = transition;
            }
        }


        /// <summary>
        /// Gets the transition that is used to navigate from one page to another.
        /// </summary>
        /// <param name="from">Type of page to navigate from.</param>
        /// <param name="to">Type of page to navigate to.</param>
        /// <returns>PageTransition to use, otherwise null.</returns>
        public static PageTransition GetTransition(Type from, Type to)
        {
            if (from == null) throw new ArgumentNullException("from");
            if (to == null) throw new ArgumentNullException("to");

            PageTransition transition;

            PageTypeKey key = new PageTypeKey(from, to);
            if (pageTransitions.TryGetValue(key, out transition)) return transition;

            // find the first usable transition from interited types:
            Type pageType = typeof(Page);
            for (; ; )
            {
                transition = GetNextTransition(from, to, key);
                if (transition != null) return transition;

                Type baseType = to.BaseType;
                if (!pageType.IsAssignableFrom(baseType)) return null;

                to = baseType;
                key.Set(from, to);
                if (pageTransitions.TryGetValue(key, out transition)) return transition;

            }
        }

        private static PageTransition GetNextTransition(Type from, Type to, PageTypeKey key)
        {
            from = from.BaseType;
            while (typeof(Page).IsAssignableFrom(from))
            {
                PageTransition transition;
                key.Set(from, to);

                if (pageTransitions.TryGetValue(key, out transition)) return transition;
                from = from.BaseType;
            }
            return null;
        }

        #endregion
    }
}
